package com.doudou.oauth.token;

import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.JdkSerializationStrategy;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStoreSerializationStrategy;

import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author 傻男人
 * @date 2020-05-08 22:52
 * @describe RedisTemplate 模板
 */
public class RedisTemplateTokenStore implements TokenStore {

    private static final String ACCESS = "access:";
    private static final String AUTH_TO_ACCESS = "auth_to_access:";
    private static final String AUTH = "auth:";
    private static final String REFRESH_AUTH = "refresh_auth:";
    private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
    private static final String REFRESH = "refresh:";
    private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
    private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
    private static final String UNAME_TO_ACCESS = "uname_to_access:";

    RedisTemplate<String,Object> redisTemplate;
    private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
    private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
    private String prefix = "";



    public RedisTemplateTokenStore(RedisConnectionFactory connectionFactory){
        redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.afterPropertiesSet();
    }


    private byte[] serialize(Object object) {
        return serializationStrategy.serialize(object);
    }

    private String serializeKey(String object) {
        return prefix + object;
    }

    private OAuth2AccessToken deserializeAccessToken(byte[] bytes) {
        return serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
    }

    private OAuth2Authentication deserializeAuthentication(byte[] bytes) {
        return serializationStrategy.deserialize(bytes, OAuth2Authentication.class);
    }

    private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) {
        return serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class);
    }

    private byte[] serialize(String string) {
        return serializationStrategy.serialize(string);
    }

    private String deserializeString(byte[] bytes) {
        return serializationStrategy.deserializeString(bytes);
    }


    @Override
    public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
        return readAuthentication(token.getValue());
    }

    @Override
    public OAuth2Authentication readAuthentication(String token) {
        return (OAuth2Authentication) redisTemplate.opsForValue().get(serializeKey(AUTH + token));
    }

    @Override
    public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
        String  accessKey = serializeKey(ACCESS + token.getValue());
        String  authKey = serializeKey(AUTH + token.getValue());
        String  authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication));
        String  approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
        String  clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());

        redisTemplate.opsForValue().set(accessKey,token);
        redisTemplate.opsForValue().set(authKey,authentication);
        redisTemplate.opsForValue().set(authToAccessKey,token);
        if(!authentication.isClientOnly()){
            redisTemplate.opsForList().rightPush(approvalKey,token);
        }
        redisTemplate.opsForList().rightPush(clientId,token);
        if (token.getExpiration() != null) {
            int seconds = token.getExpiresIn();
            redisTemplate.expire(accessKey, seconds, TimeUnit.SECONDS);
            redisTemplate.expire(authKey, seconds,TimeUnit.SECONDS);
            redisTemplate.expire(authToAccessKey, seconds,TimeUnit.SECONDS);
            redisTemplate.expire(clientId, seconds,TimeUnit.SECONDS);
            redisTemplate.expire(approvalKey, seconds,TimeUnit.SECONDS);
        }
        OAuth2RefreshToken refreshToken = token.getRefreshToken();
        if (refreshToken != null && refreshToken.getValue() != null) {
            String refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue());
            redisTemplate.opsForValue().set(refreshToAccessKey,token.getValue());
            String accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + token.getValue());
            redisTemplate.opsForValue().set(accessToRefreshKey,token.getRefreshToken().getValue());
            if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
                Date expiration = expiringRefreshToken.getExpiration();
                if (expiration != null) {
                    int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
                            .intValue();
                    redisTemplate.expire(refreshToAccessKey, seconds,TimeUnit.SECONDS);
                    redisTemplate.expire(accessToRefreshKey, seconds,TimeUnit.SECONDS);
                }
            }
        }

    }

    @Override
    public OAuth2AccessToken readAccessToken(String tokenValue) {
        OAuth2AccessToken accessToken = (OAuth2AccessToken) redisTemplate.opsForValue().get(ACCESS + tokenValue);;
        return accessToken;
    }

    @Override
    public void removeAccessToken(OAuth2AccessToken accessToken) {
        removeAccessToken(accessToken.getValue());
    }

    public void removeAccessToken(String tokenValue) {
        String accessKey=ACCESS + tokenValue;
        String authKey= AUTH +tokenValue;
        String accessToRefreshKey = ACCESS_TO_REFRESH + tokenValue;

        Object access = redisTemplate.opsForValue().get(accessKey);
        Object auth = redisTemplate.opsForValue().get(authKey);
        redisTemplate.delete(accessKey);
        redisTemplate.delete(authKey);
        redisTemplate.delete(accessToRefreshKey);

        OAuth2Authentication authentication = (OAuth2Authentication) auth;

        if (authentication != null) {
            String key = authenticationKeyGenerator.extractKey(authentication);
            String authToAccessKey = AUTH_TO_ACCESS + key;
            String unameKey = UNAME_TO_ACCESS + getApprovalKey(authentication);
            String clientId = CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId();
            redisTemplate.delete(authToAccessKey);

            redisTemplate.opsForList().rightPush(unameKey,access);
            redisTemplate.opsForList().rightPush(clientId,access);

            redisTemplate.delete(ACCESS + key);
        }
    }

    @Override
    public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {

        String refreshKey = REFRESH + refreshToken.getValue();
        String refreshAuthKey = REFRESH_AUTH + refreshToken.getValue();
        redisTemplate.opsForValue().set(refreshKey,refreshToken);
        redisTemplate.opsForValue().set(refreshAuthKey,authentication);

        if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
            Date expiration = expiringRefreshToken.getExpiration();
            if (expiration != null) {
                int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
                        .intValue();
                redisTemplate.expire(refreshKey,seconds, TimeUnit.SECONDS);
                redisTemplate.expire(refreshAuthKey,seconds, TimeUnit.SECONDS);
            }
        }

    }

    @Override
    public OAuth2RefreshToken readRefreshToken(String tokenValue) {
        return (OAuth2RefreshToken) redisTemplate.opsForValue().get(REFRESH + tokenValue);
    }

    @Override
    public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
        return readAuthenticationForRefreshToken(token.getValue());
    }

    public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
        return (OAuth2Authentication) redisTemplate.opsForValue().get(REFRESH_AUTH + token);
    }

    @Override
    public void removeRefreshToken(OAuth2RefreshToken refreshToken) {
        removeRefreshToken(refreshToken.getValue());
    }
    public void removeRefreshToken(String tokenValue) {
        String refreshKey =REFRESH + tokenValue;
        String refreshAuthKey = REFRESH_AUTH + tokenValue;
        String refresh2AccessKey = REFRESH_TO_ACCESS + tokenValue;
        String access2RefreshKey = ACCESS_TO_REFRESH + tokenValue;

        redisTemplate.delete(refreshKey);
        redisTemplate.delete(refreshAuthKey);
        redisTemplate.delete(refresh2AccessKey);
        redisTemplate.delete(access2RefreshKey);

    }

    @Override
    public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
        removeAccessTokenUsingRefreshToken(refreshToken.getValue());
    }

    private void removeAccessTokenUsingRefreshToken(String refreshToken) {
        String key = REFRESH_TO_ACCESS + refreshToken;
        String accessToken = (String) redisTemplate.opsForValue().get(key);
        if(accessToken!=null){
            redisTemplate.delete(key);
            removeAccessToken(accessToken);
        }
    }

    @Override
    public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
        String key = authenticationKeyGenerator.extractKey(authentication);
        String serializedKey = AUTH_TO_ACCESS + key;

        OAuth2AccessToken accessToken = (OAuth2AccessToken) redisTemplate.opsForValue().get(serializedKey);
        if (accessToken != null
                && !key.equals(authenticationKeyGenerator.extractKey(readAuthentication(accessToken.getValue())))) {
            // Keep the stores consistent (maybe the same user is
            // represented by this authentication but the details have
            // changed)
            storeAccessToken(accessToken, authentication);
        }
        return accessToken;
    }

    @Override
    public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
        String approvalKey = UNAME_TO_ACCESS + getApprovalKey(clientId, userName);

        List<Object> accessTokens = redisTemplate.opsForList().range(approvalKey,0,-1);

        if(accessTokens ==null || accessTokens.isEmpty()){
            return Collections.<OAuth2AccessToken> emptySet();
        }

        return  Collections.<OAuth2AccessToken> unmodifiableCollection(
                accessTokens.stream().map(token->(OAuth2AccessToken)token)
                        .collect(Collectors.toList()));


    }

    @Override
    public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
        String key =CLIENT_ID_TO_ACCESS + clientId;
        List<Object> accessTokens = redisTemplate.opsForList().range(key,0,-1);
        if(accessTokens ==null || accessTokens.isEmpty()){
            return Collections.<OAuth2AccessToken> emptySet();
        }
        return  Collections.<OAuth2AccessToken> unmodifiableCollection(
                accessTokens.stream().map(token->(OAuth2AccessToken)token)
                        .collect(Collectors.toList()));

    }

    private static String getApprovalKey(OAuth2Authentication authentication) {
        String userName = authentication.getUserAuthentication() == null ? ""
                : authentication.getUserAuthentication().getName();
        return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);
    }

    private static String getApprovalKey(String clientId, String userName) {
        return clientId + (userName == null ? "" : ":" + userName);
    }



}
